﻿using Dorado.Core;
using Dorado.Core.Collection;
using Dorado.Extensions;
using Dorado.Platform.FileSystems.WebSite;
using Dorado.Platform.Infrastructure;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;

namespace Dorado.Platform.Themes
{
    public partial class DefaultThemeRegistry : DisposableObject, IThemeRegistry
    {
        #region Fields

        private readonly bool _enableMonitoring;
        private readonly string _themesBasePath;
        private readonly ConcurrentDictionary<string, ThemeManifest> _themes = new ConcurrentDictionary<string, ThemeManifest>(StringComparer.InvariantCultureIgnoreCase);
        private readonly ConcurrentDictionary<EventThrottleKey, Timer> _eventQueue = new ConcurrentDictionary<EventThrottleKey, Timer>();

        private readonly Regex _fileFilterPattern = new Regex(@"^\.(config|png|gif|jpg|jpeg|css|less|js|cshtml|svg|json)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);

        private FileSystemWatcher _monitorFolders;
        private FileSystemWatcher _monitorFiles;

        #endregion Fields

        #region Constructors

        public DefaultThemeRegistry(bool? enableMonitoring, string themesBasePath, bool autoLoadThemes)
        {
            this._enableMonitoring = enableMonitoring ?? CommonHelper.GetAppSetting("MonitorThemesFolder", true);
            this._themesBasePath = themesBasePath.NullEmpty() ?? CommonHelper.GetAppSetting<string>("ThemesBasePath", "~/Themes/").EnsureEndsWith("/");

            if (autoLoadThemes)
            {
                // load all themes initially
                ReloadThemes();
            }

            CreateFileSystemWatchers();

            // start FS watcher
            this.StartMonitoring(false);
        }

        #endregion Constructors

        #region IThemeRegistry

        public bool ThemeManifestExists(string themeName)
        {
            if (themeName.IsEmpty())
                return false;

            ThemeManifest manifest;
            if (_themes.TryGetValue(themeName, out manifest))
            {
                return manifest.State == ThemeManifestState.Active;
            }

            return false;
        }

        public ThemeManifest GetThemeManifest(string themeName)
        {
            ThemeManifest value;
            if (themeName.HasValue() && _themes.TryGetValue(themeName, out value))
            {
                return value;
            }
            return null;
        }

        public ICollection<ThemeManifest> GetThemeManifests(bool includeHidden = false)
        {
            var allThemes = _themes.Values;

            if (includeHidden)
            {
                return allThemes.AsReadOnly();
            }
            else
            {
                return allThemes.Where(x => x.State == ThemeManifestState.Active).AsReadOnly();
            }
        }

        public void AddThemeManifest(ThemeManifest manifest)
        {
            AddThemeManifestInternal(manifest, false);
        }

        private void AddThemeManifestInternal(ThemeManifest manifest, bool isInit)
        {
            Guard.ArgumentNotNull(() => manifest);

            if (!isInit)
            {
                TryRemoveManifest(manifest.ThemeName);
            }

            ThemeManifest baseManifest = null;
            if (manifest.BaseThemeName != null)
            {
                if (!_themes.TryGetValue(manifest.BaseThemeName, out baseManifest))
                {
                    manifest.State = ThemeManifestState.MissingBaseTheme;
                }
            }

            manifest.BaseTheme = baseManifest;
            var added = _themes.TryAdd(manifest.ThemeName, manifest);
            if (added && !isInit)
            {
                // post process
                var children = GetChildrenOf(manifest.ThemeName, false);
                foreach (var child in children)
                {
                    child.BaseTheme = manifest;
                    child.State = ThemeManifestState.Active;
                }
            }
        }

        [SuppressMessage("ReSharper", "AssignmentInConditionalExpression")]
        private bool TryRemoveManifest(string themeName)
        {
            bool result;
            ThemeManifest existing;
            if (result = _themes.TryRemove(themeName, out existing))
            {
                existing.BaseTheme = null;

                // set all direct children as broken
                var children = GetChildrenOf(themeName, false);
                foreach (var child in children)
                {
                    child.BaseTheme = null;
                    child.State = ThemeManifestState.MissingBaseTheme;
                }
            }

            return result;
        }

        public bool IsChildThemeOf(string themeName, string baseTheme)
        {
            if (themeName.IsEmpty() && baseTheme.IsEmpty())
            {
                return false;
            }

            if (themeName.Equals(baseTheme, StringComparison.OrdinalIgnoreCase))
            {
                return false;
            }

            var current = GetThemeManifest(themeName);
            if (current == null)
                return false;

            while (current.BaseThemeName != null)
            {
                if (baseTheme.Equals(current.BaseThemeName, StringComparison.OrdinalIgnoreCase))
                {
                    return true;
                }

                if (!_themes.TryGetValue(current.BaseThemeName, out current))
                {
                    return false;
                }
                //currentBaseName = current.BaseThemeName;
            }

            return false;
        }

        public IEnumerable<ThemeManifest> GetChildrenOf(string themeName, bool deep = true)
        {
            Guard.ArgumentNotEmpty(() => themeName);

            if (!ThemeManifestExists(themeName))
                Enumerable.Empty<ThemeManifest>();

            var derivedThemes = _themes.Values.Where(x => x.BaseThemeName != null && !x.ThemeName.IsCaseInsensitiveEqual(themeName));
            if (!deep)
            {
                derivedThemes = derivedThemes.Where(x => x.BaseThemeName.IsCaseInsensitiveEqual(themeName));
            }
            else
            {
                derivedThemes = derivedThemes.Where(x => IsChildThemeOf(x.ThemeName, themeName));
            }

            return derivedThemes;
        }

        public void ReloadThemes()
        {
            _themes.Clear();

            var folder = DoradoContext.Instance.ContainerManager.Resolve<IWebSiteFolder>();
            var folderDatas = new List<ThemeFolderData>();
            var dirs = folder.ListDirectories(_themesBasePath);

            // create folder (meta)datas first
            foreach (var path in dirs)
            {
                try
                {
                    var folderData = ThemeManifest.CreateThemeFolderData(CommonHelper.MapPath(path), _themesBasePath);
                    if (folderData != null)
                    {
                        folderDatas.Add(folderData);
                    }
                }
                catch (Exception ex)
                {
                    Debug.WriteLine("ERR - unable to create folder data for folder '{0}': {1}".FormatCurrent(path, ex.Message));
                }
            }

            // perform topological sort (BaseThemes first...)
            IEnumerable<ThemeFolderData> sortedThemeFolders;
            try
            {
                sortedThemeFolders = folderDatas.ToArray().SortTopological(StringComparer.OrdinalIgnoreCase).Cast<ThemeFolderData>();
            }
            catch (CyclicDependencyException)
            {
                throw new CyclicDependencyException("Cyclic theme dependencies detected. Please check the 'baseTheme' attribute of your themes and ensure that they do not reference themselves (in)directly.");
            }
            catch
            {
                throw;
            }

            // create theme manifests
            foreach (var themeFolder in sortedThemeFolders)
            {
                try
                {
                    var manifest = ThemeManifest.Create(themeFolder);
                    if (manifest != null)
                    {
                        AddThemeManifestInternal(manifest, true);
                    }
                }
                catch (Exception ex)
                {
                    Debug.WriteLine("ERR - unable to create manifest for theme '{0}': {1}".FormatCurrent(themeFolder.FolderName, ex.Message));
                }
            }
        }

        #endregion IThemeRegistry

        #region Monitoring & Events

        private void CreateFileSystemWatchers()
        {
            _monitorFiles = new FileSystemWatcher();
            _monitorFiles.Path = CommonHelper.MapPath(_themesBasePath);
            _monitorFiles.InternalBufferSize = 32768; // 32 instead of the default 8 KB
            _monitorFiles.Filter = "*.*";
            _monitorFiles.NotifyFilter = NotifyFilters.CreationTime | NotifyFilters.LastWrite | NotifyFilters.FileName;
            _monitorFiles.IncludeSubdirectories = true;
            _monitorFiles.Changed += (s, e) => OnThemeFileChanged(e.Name, e.FullPath, ThemeFileChangeType.Modified);
            _monitorFiles.Deleted += (s, e) => OnThemeFileChanged(e.Name, e.FullPath, ThemeFileChangeType.Deleted);
            _monitorFiles.Created += (s, e) => OnThemeFileChanged(e.Name, e.FullPath, ThemeFileChangeType.Created);
            _monitorFiles.Renamed += (s, e) =>
            {
                OnThemeFileChanged(e.OldName, e.OldFullPath, ThemeFileChangeType.Deleted);
                OnThemeFileChanged(e.Name, e.FullPath, ThemeFileChangeType.Created);
            };

            _monitorFolders = new FileSystemWatcher();
            _monitorFolders.Path = CommonHelper.MapPath(_themesBasePath);
            _monitorFolders.Filter = "*";
            _monitorFolders.NotifyFilter = NotifyFilters.DirectoryName;
            _monitorFolders.IncludeSubdirectories = false;
            _monitorFolders.Renamed += (s, e) => OnThemeFolderRenamed(e.Name, e.FullPath, e.OldName, e.OldFullPath);
            _monitorFolders.Deleted += (s, e) => OnThemeFolderDeleted(e.Name, e.FullPath);
        }

        public void StartMonitoring(bool force)
        {
            var shouldStart = force || _enableMonitoring;

            if (shouldStart && !_monitorFiles.EnableRaisingEvents)
                _monitorFiles.EnableRaisingEvents = true;
            if (shouldStart && !_monitorFolders.EnableRaisingEvents)
                _monitorFolders.EnableRaisingEvents = true;
        }

        public void StopMonitoring()
        {
            if (_monitorFiles.EnableRaisingEvents)
                _monitorFiles.EnableRaisingEvents = false;
            if (_monitorFolders.EnableRaisingEvents)
                _monitorFolders.EnableRaisingEvents = false;
        }

        private bool ShouldThrottleEvent(EventThrottleKey key)
        {
            Timer timer;
            if (_eventQueue.TryGetValue(key, out timer))
            {
                // do nothing. The same event was published a tick ago.
                return true;
            }

            _eventQueue[key] = new Timer(RemoveFromEventQueue, key, 500, Timeout.Infinite);
            return false;
        }

        private void RemoveFromEventQueue(object key)
        {
            Timer timer;
            if (_eventQueue.TryRemove((EventThrottleKey)key, out timer))
            {
                timer.Dispose();
            }
        }

        private void WaitForUnlock(string fullPath)
        {
            try
            {
                var fi = new FileInfo(fullPath);
                for (var i = 0; i < 5; i++)
                {
                    if (!fi.IsFileLocked())
                    {
                        return;
                    }
                    Thread.Sleep(50);
                }
            }
            finally { }
        }

        private void OnThemeFileChanged(string name, string fullPath, ThemeFileChangeType changeType)
        {
            // Enable event throttling by allowing the very same event to be published only all 500 ms.
            var throttleKey = new EventThrottleKey(name, changeType);
            if (ShouldThrottleEvent(throttleKey))
            {
                return;
            }

            if (!_fileFilterPattern.IsMatch(Path.GetExtension(name)))
                return;

            var idx = name.IndexOf('\\');
            if (idx < 0)
            {
                // must be a subfolder of "~/Themes/"
                return;
            }

            var themeName = name.Substring(0, idx);
            var relativePath = name.Substring(themeName.Length + 1).Replace('\\', '/');
            var isConfigFile = relativePath.IsCaseInsensitiveEqual("theme.config");

            if (changeType == ThemeFileChangeType.Modified && !isConfigFile)
            {
                // Monitor changes only for root theme.config
                return;
            }

            BaseThemeChangedEventArgs baseThemeChangedArgs = null;

            if (isConfigFile)
            {
                // config file changes always result in refreshing the corresponding theme manifest
                var di = new DirectoryInfo(Path.GetDirectoryName(fullPath));

                string oldBaseThemeName = null;
                var oldManifest = this.GetThemeManifest(di.Name);
                if (oldManifest != null)
                {
                    oldBaseThemeName = oldManifest.BaseThemeName;
                }

                try
                {
                    // FS watcher in conjunction with some text editors fires change events twice and locks the file.
                    // Let's wait max. 250 ms till the lock is gone (hopefully).
                    WaitForUnlock(fullPath);

                    var newManifest = ThemeManifest.Create(di.FullName, _themesBasePath);
                    if (newManifest != null)
                    {
                        this.AddThemeManifestInternal(newManifest, false);

                        if (!oldBaseThemeName.IsCaseInsensitiveEqual(newManifest.BaseThemeName))
                        {
                            baseThemeChangedArgs = new BaseThemeChangedEventArgs
                            {
                                ThemeName = newManifest.ThemeName,
                                BaseTheme = newManifest.BaseTheme != null ? newManifest.BaseTheme.ThemeName : null,
                                OldBaseTheme = oldBaseThemeName
                            };
                        }

                        Debug.WriteLine("Changed theme manifest for '{0}'".FormatCurrent(name));
                    }
                    else
                    {
                        // something went wrong (most probably no 'theme.config'): remove the manifest
                        TryRemoveManifest(di.Name);
                    }
                }
                catch (Exception ex)
                {
                    TryRemoveManifest(di.Name);
                    Debug.WriteLine("ERR - Could not touch theme manifest '{0}': {1}".FormatCurrent(name, ex.Message));
                }
            }

            if (baseThemeChangedArgs != null)
            {
                RaiseBaseThemeChanged(baseThemeChangedArgs);
            }

            RaiseThemeFileChanged(new ThemeFileChangedEventArgs
            {
                ChangeType = changeType,
                FullPath = fullPath,
                ThemeName = themeName,
                RelativePath = relativePath,
                IsConfigurationFile = isConfigFile
            });
        }

        private void OnThemeFolderRenamed(string name, string fullPath, string oldName, string oldFullPath)
        {
            TryRemoveManifest(oldName);

            try
            {
                var newManifest = GetThemeManifest(name);
                if (newManifest != null)
                {
                    this.AddThemeManifestInternal(newManifest, false);
                    Debug.WriteLine("Changed theme manifest for '{0}'".FormatCurrent(name));
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine("ERR - Could not touch theme manifest '{0}': {1}".FormatCurrent(name, ex.Message));
            }

            RaiseThemeFolderRenamed(new ThemeFolderRenamedEventArgs
            {
                FullPath = fullPath,
                Name = name,
                OldFullPath = oldFullPath,
                OldName = oldName
            });
        }

        private void OnThemeFolderDeleted(string name, string fullPath)
        {
            TryRemoveManifest(name);

            RaiseThemeFolderDeleted(new ThemeFolderDeletedEventArgs
            {
                FullPath = fullPath,
                Name = name
            });
        }

        public event EventHandler<ThemeFileChangedEventArgs> ThemeFileChanged;

        protected void RaiseThemeFileChanged(ThemeFileChangedEventArgs e)
        {
            if (ThemeFileChanged != null)
                ThemeFileChanged(this, e);
        }

        public event EventHandler<ThemeFolderRenamedEventArgs> ThemeFolderRenamed;

        protected void RaiseThemeFolderRenamed(ThemeFolderRenamedEventArgs e)
        {
            if (ThemeFolderRenamed != null)
                ThemeFolderRenamed(this, e);
        }

        public event EventHandler<ThemeFolderDeletedEventArgs> ThemeFolderDeleted;

        protected void RaiseThemeFolderDeleted(ThemeFolderDeletedEventArgs e)
        {
            if (ThemeFolderDeleted != null)
                ThemeFolderDeleted(this, e);
        }

        public event EventHandler<BaseThemeChangedEventArgs> BaseThemeChanged;

        protected void RaiseBaseThemeChanged(BaseThemeChangedEventArgs e)
        {
            if (BaseThemeChanged != null)
                BaseThemeChanged(this, e);
        }

        #endregion Monitoring & Events

        #region Disposable

        protected override void OnDispose(bool disposing)
        {
            if (disposing)
            {
                if (_monitorFiles != null)
                {
                    _monitorFiles.EnableRaisingEvents = false;
                    _monitorFiles.Dispose();
                    _monitorFiles = null;
                }

                if (_monitorFolders != null)
                {
                    _monitorFolders.EnableRaisingEvents = false;
                    _monitorFolders.Dispose();
                    _monitorFolders = null;
                }
            }
        }

        #endregion Disposable

        private class EventThrottleKey : Tuple<string, ThemeFileChangeType>
        {
            public EventThrottleKey(string name, ThemeFileChangeType changeType)
                : base(name, changeType)
            {
            }
        }
    }
}